/*
Copyright 2024.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package controller

import (
	"context"
	"fmt"

	kapps "k8s.io/api/apps/v1"
	corev1 "k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/fields" // Required for Watching
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/types" // Required for Watching
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/builder" // Required for Watching
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/event"
	"sigs.k8s.io/controller-runtime/pkg/handler" // Required for Watching
	"sigs.k8s.io/controller-runtime/pkg/log"
	"sigs.k8s.io/controller-runtime/pkg/predicate" // Required for Watching
	"sigs.k8s.io/controller-runtime/pkg/reconcile" // Required for Watching

	// Required for Watching
	webappv1 "gitee.com/maxtr/depreload/api/v1"
)

// 确定我们希望用作“对象引用”的Depreload CRD中字段的路径。这将用于 indexing 和 watching。
const (
	configMapField = "configMapField"
)

// DepreloadReconciler reconciles a Depreload object
type DepreloadReconciler struct {
	client.Client
	Scheme *runtime.Scheme
}

// 添加了deployments、configmaps等资源的权限
//+kubebuilder:rbac:groups=webapp.yzrnb.com,resources=depreloads,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups=webapp.yzrnb.com,resources=depreloads/status,verbs=get;update;patch
//+kubebuilder:rbac:groups=webapp.yzrnb.com,resources=depreloads/finalizers,verbs=update
//+kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups=apps,resources=deployments/status,verbs=get
//+kubebuilder:rbac:groups="",resources=configmaps,verbs=get;list;watch

// Reconcile is part of the main kubernetes reconciliation loop which aims to
// move the current state of the cluster closer to the desired state.
// TODO(user): Modify the Reconcile function to compare the state specified by
// the Depreload object against the actual cluster state, and then
// perform operations to make the cluster state reflect the state specified by
// the user.
//
// For more details, check Reconcile and its Result here:
// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.16.3/pkg/reconcile
// Reconcile 将负责协调Depreload的状态。Depreloads用于管理Deployments，其Pod在其所使用的configMap更新时更新。
// 出于这个原因，我们需要在我们创建的Deployment中向PodTemplate添加一个注释。此注释将跟踪引用的ConfigMap中数据的最新版本。因此，当部署映射的版本发生变化时，部署中的PodTemplate也会发生变化。这将导致Deployment管理的所有Pod进行滚动升级。
func (r *DepreloadReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
	log := log.FromContext(ctx)
	log.Info("进入reconcile...")

	var Depreload webappv1.Depreload
	if err := r.Get(ctx, req.NamespacedName, &Depreload); err != nil {
		log.Error(err, "unable to fetch Depreload")
		return ctrl.Result{}, client.IgnoreNotFound(err)
	}

	// 遍历自定义类型Depreload中的spec
dep:
	// 遍历deployment
	for _, e := range Depreload.Spec.Endpoint {
		// 声明一个空的annotation，用于存储后面获取的deployment的annotation
		var annotations = map[string]string{}
		foundDeployment := &kapps.Deployment{}
		if err := r.Get(ctx, types.NamespacedName{Name: e.Deployment, Namespace: Depreload.Namespace}, foundDeployment); err != nil {
			if client.IgnoreNotFound(err) != nil {
				return ctrl.Result{}, err
			} else {
				log.V(1).Info("deployment未找到", "deploymentName", e.Deployment)
				continue dep
			}
		}
		// 遍历configmap，获取configmap的version
	conf:
		for _, configMapName := range e.ConfigMaps {
			if configMapName != "" && e.Deployment != "" {
				foundConfigMap := &corev1.ConfigMap{}
				if err := r.Get(ctx, types.NamespacedName{Name: configMapName, Namespace: Depreload.Namespace}, foundConfigMap); err != nil {
					if client.IgnoreNotFound(err) != nil {
						return ctrl.Result{}, err
					} else {
						log.V(1).Info("configmap未找到", "deploymentName", e.Deployment, "configMapName", configMapName)
						continue conf
					}
				}
				configMapVersion := foundConfigMap.ResourceVersion
				var annotationName = fmt.Sprint(configMapName + "-version")
				// configmap有更新，则添加到annotations组里面
				if ver := foundDeployment.Spec.Template.GetAnnotations()[annotationName]; ver != configMapVersion {
					log.V(1).Info("configmap version不一致", "configmapName", configMapName, "deployment中的version", ver, "当前configmap version", configMapVersion)
					annotations[annotationName] = configMapVersion
				}
			}
		}
		// 如果得到的annotations长度大于0，更新deployment
		if len(annotations) > 0 {
			foundAnnotations := foundDeployment.Spec.Template.GetAnnotations()
			for key, value := range annotations {
				foundAnnotations[key] = value
			}
			log.Info("开始更新annotations", "annotations", foundAnnotations, "length", len(annotations))
			foundDeployment.Spec.Template.SetAnnotations(foundAnnotations)
			if err := r.Update(ctx, foundDeployment); err != nil {
				if client.IgnoreNotFound(err) == nil {
					log.V(1).Info("更新deployment未找到", "deploymentName", e.Deployment)
				}
				log.Error(err, "更新deployment失败")
				return ctrl.Result{}, err
			}
		}
	}

	return ctrl.Result{}, nil
}

// 最后，我们将这个协调器添加到管理器中，以便它在管理器启动时启动。
// SetupWithManager 函数确保在引用的 ConfigMaps 被更新时调用 Reconcile 。
// SetupWithManager sets up the controller with the Manager.
func (r *DepreloadReconciler) SetupWithManager(mgr ctrl.Manager) error {
	// configMap 字段必须由管理器索引，以便我们能够通过引用的 ConfigMap 名称查找 Depreloads
	// 如果更新了ConfigMap x，哪些Depreload会受到影响？
	if err := mgr.GetFieldIndexer().IndexField(context.Background(), &webappv1.Depreload{}, configMapField, func(rawObj client.Object) []string {
		// Extract the ConfigMap name from the Depreload Spec, if one is provided
		Depreload := rawObj.(*webappv1.Depreload)
		var cfl []string
		for _, e := range Depreload.Spec.Endpoint {
			for _, configMapName := range e.ConfigMaps {
				if configMapName != "" {
					cfl = append(cfl, configMapName)
				}
			}
		}
		return cfl
	}); err != nil {
		return err
	}
	// 我们要监视的ConfigMaps不属于Depreload对象。因此，我们必须指定一种自定义的方式来查看这些对象。
	// 控制器将首先注册它管理的Type以及它控制的子资源的类型。由于我们还想监视不受控制器控制或管理的ConfigMap，因此我们还需要使用 Watches() 功能
	// Watches() 函数是一个controller-runtime API，它接受：
	//	1. A Kind (i.e. ConfigMap)
	//	2. 一个映射函数，用于将 ConfigMap 对象转换为 Depreloads 的协调请求列表。我们将其分离为一个单独的功能。
	//	3. watching ConfigMaps 的选项列表
	// 在本例中，我们只希望在ConfigMap的ResourceVersion更新或删除时触发watch
	return ctrl.NewControllerManagedBy(mgr).
		For(&webappv1.Depreload{}).
		Owns(&kapps.Deployment{}).
		Watches(
			&corev1.ConfigMap{},
			handler.EnqueueRequestsFromMapFunc(r.findObjectsForConfigMap),
			builder.WithPredicates(predicate.Funcs{
				UpdateFunc: func(e event.UpdateEvent) bool {
					return true
				},
				CreateFunc: func(e event.CreateEvent) bool {
					return false
				},
				DeleteFunc: func(e event.DeleteEvent) bool {
					return true
				},
				GenericFunc: func(e event.GenericEvent) bool {
					return false
				},
			}),
		).
		Complete(r)
}

// 通过使用我们的索引字段作为字段选择器提交一个List请求
// 当找到引用 ConfigMap 的 Depreloads 的列表时，我们只需要遍历该列表并为每个列表创建一个协调请求。如果在获取列表时发生错误，或者未找到 Depreloads ，则不会返回协调请求
func (r *DepreloadReconciler) findObjectsForConfigMap(ctx context.Context, configMap client.Object) []reconcile.Request {
	attachedDepreloads := &webappv1.DepreloadList{}
	listOps := &client.ListOptions{
		FieldSelector: fields.OneTermEqualSelector(configMapField, configMap.GetName()),
		Namespace:     configMap.GetNamespace(),
	}
	err := r.List(ctx, attachedDepreloads, listOps)
	if err != nil {
		return []reconcile.Request{}
	}

	requests := make([]reconcile.Request, len(attachedDepreloads.Items))
	for i, item := range attachedDepreloads.Items {
		requests[i] = reconcile.Request{
			NamespacedName: types.NamespacedName{
				Name:      item.GetName(),
				Namespace: item.GetNamespace(),
			},
		}
	}
	return requests
}
